package bsh.compilation;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import bsh.ReflectionUtils;
import bsh.exception.CompilationAssertionFailed;
import bsh.meta.ArrayTypeWrapper;
import bsh.meta.FieldDescriptor;
import bsh.meta.JavaClassWrapper;
import bsh.meta.JavaMethodDescriptor;
import bsh.meta.MethodDescriptor;
import bsh.meta.TypeTransformation;
import bsh.meta.TypeWrapper;
import bsh.utils.reflection.transform.BoxTypeTransformation;
import bsh.utils.reflection.transform.DynamicWrapperTypeTransformation;
import bsh.utils.reflection.transform.NullTypeTransformationFactory;
import bsh.utils.reflection.transform.PrimitiveWideningTypeTransformations;
import bsh.utils.reflection.transform.TypeTransformationFactory;
import bsh.utils.reflection.transform.UnboxingTypeTransformation;

/**
 * @author takeshi
 */
public class ReflectionUtilsImpl implements ReflectionUtils {

    private final List<TypeTransformation> allTransformations = new LinkedList<TypeTransformation>();

    private final List<TypeTransformationFactory> transformationFactories = new LinkedList<TypeTransformationFactory>();

    private final Map<ClassLoader, Map<String, Object>> typesForName = new HashMap<ClassLoader, Map<String, Object>>();

    public static final Object NOT_PRESENT = new Object();

    /**
     * C'tor
     */
    public ReflectionUtilsImpl() {
        this.fillTransformations();
    }

    /**
     * 
     */
    private void fillTransformations() {
        this.allTransformations.add(new BoxTypeTransformation());
        this.allTransformations.add(new UnboxingTypeTransformation());
        this.allTransformations.addAll(PrimitiveWideningTypeTransformations.allTransformations());
        this.allTransformations.add(new DynamicWrapperTypeTransformation());
        this.transformationFactories.add(BigDecimalTransformations.DOUBLE_TO_BIG_DECIMAL);
        this.transformationFactories.add(BigDecimalTransformations.LONG_TO_BIG_DECIMAL);
        this.transformationFactories.add(new NullTypeTransformationFactory());
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#findMethodForTypes(bsh.meta.TypeWrapper, java.lang.String, bsh.meta.TypeWrapper[])
     */
    public MethodDescriptor findMethodForTypes(TypeWrapper declaring, String name, TypeWrapper[] argTypes) {
        List<MethodDescriptor> candidates = new ArrayList<MethodDescriptor>();
        Collection<? extends MethodDescriptor> methods = declaring.visibleMethods();
        for (MethodDescriptor method : methods) {
            if (method.methodName().equals(name) && method.parameterTypes().size() == argTypes.length) {
                candidates.add(method);
            }
        }

        Collections.sort(candidates, new Comparator<MethodDescriptor>() {

            public int compare(MethodDescriptor o1, MethodDescriptor o2) {
                boolean leftIsInterface = o1.declaringType().isInterface();
                if (leftIsInterface == o2.declaringType().isInterface()) {
                    return 0;
                }
                if (leftIsInterface) {
                    return 1;
                }
                return -1;
            }
        });
        List<List<? extends TypeWrapper>> params = new LinkedList<List<? extends TypeWrapper>>();
        for (MethodDescriptor methodDescriptor : candidates) {
            params.add(methodDescriptor.parameterTypes());
        }

        int index = this.findBestMatch(params, argTypes);
        if (index >= 0) {
            return candidates.get(index);
        }

        // have to try with varargs
        candidates.clear();
        params.clear();
        for (MethodDescriptor method : methods) {
            if (method.isVarArgs() && method.methodName().equals(name) && method.parameterTypes().size() >= argTypes.length - 1) {
                candidates.add(method);
                params.add(method.parameterTypes());
            }
        }
        index = this.findBestMatchWithVarargs(params, argTypes);
        if (index >= 0) {
            return candidates.get(index);
        }
        return null;
    }

    /**
     * @param methodsArguments list de {@link TypeWrapper}
     * @param argTypes {@link TypeWrapper}
     * @return int
     */
    private int findBestMatchWithVarargs(List<List<? extends TypeWrapper>> methodsArguments, TypeWrapper[] argTypes) {
        for (int depth = 0; depth < MAX_DEPTH; depth++) {
            for (int index = 0; index < methodsArguments.size(); index++) {
                List<? extends TypeWrapper> methodParams = methodsArguments.get(index);
                if (this.isTypesTransformable(argTypes, methodParams, depth, methodParams.size() - 1)) {
                    int fromIndex = methodParams.size() - 1;
                    TypeWrapper varargsType = ((ArrayTypeWrapper) methodParams.get(methodParams.size() - 1)).getComponentType();
                    if (this.fitsIntoArray(argTypes, depth, fromIndex, varargsType)) {
                        return index;
                    }
                }
            }
        }
        return -1;
    }

    /**
     * Verifica se os tipos podem ser converitos em um array. Para tratamento de varargs
     * 
     * @param argTypes {@link TypeWrapper}
     * @param depth int
     * @param fromIndex int
     * @param arrayType int
     * @return boolean
     */
    private boolean fitsIntoArray(TypeWrapper[] argTypes, int depth, int fromIndex, TypeWrapper arrayType) {
        for (int i = fromIndex; i < argTypes.length; i++) {
            TypeWrapper oneType = argTypes[i];
            if (!this.isTransformable(oneType, arrayType, depth)) {
                return false;
            }
        }
        return true;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#resolveType(java.lang.String)
     */
    public TypeWrapper resolveType(String name) {
        final Class<?> resolved = this.resolveTypeWithDeps(name);
        if (resolved == null) {
            return null;
        }
        return JavaClassWrapper.wrapperFor(resolved);
    }

    /**
     * @param text String
     * @return Class
     */
    public Class<?> resolveTypeWithDeps(String text) {
        final Class<?> type = resolveTypeInClassLoader(text, Thread.currentThread().getContextClassLoader());
        if (type == null) {
            return resolveTypeInClassLoader(text, this.getClass().getClassLoader());
        }
        return type;
    }

    /**
     * @param loader ClassLoader
     * @return Map
     */
    private Map<String, Object> cacheForClassLoader(ClassLoader loader) {
        Map<String, Object> map = this.typesForName.get(loader);
        if (map == null) {
            map = new HashMap<String, Object>();
            this.typesForName.put(loader, map);
        }
        return map;
    }

    /**
     * @param text String
     * @param classLoader {@link ClassLoader}
     * @return {@link Class}
     */
    private Class<?> resolveTypeInClassLoader(String text, final ClassLoader classLoader) {
        final Map<String, Object> cache = cacheForClassLoader(classLoader);
        Object cached = cache.get(text);
        if (cached != null) {
            if (cached == NOT_PRESENT) {
                return null;
            }
            return (Class<?>) cached;
        }
        try {
            final Class<?> cl = Class.forName(text, false, classLoader);
            cache.put(text, cl);
            return cl;
        } catch (ClassNotFoundException e) {
            // ignore
        } catch(NoClassDefFoundError e) {
        	//ignore
        }
        // maybe dependent type? lets try it
        int i = text.lastIndexOf('.');
        if (i < 0) {
            // not a dependent type.
            cache.put(text, NOT_PRESENT);
            return null;
        }
        String preffix = text.substring(0, i);
        Class<?> owner = this.resolveTypeWithDeps(preffix);
        if (owner != null) {
            Class<?>[] declaredClasses = owner.getDeclaredClasses();
            for (Class<?> class1 : declaredClasses) {
                String alias = class1.getName().substring(class1.getName().lastIndexOf('$') + 1, class1.getName().length());
                if (text.equals(preffix + "." + alias)) {
                    cache.put(text, class1);
                    return class1;
                }
            }
        }
        cache.put(text, NOT_PRESENT);
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#findBestMatch(java.util.List, bsh.meta.TypeWrapper[])
     */
    public int findBestMatch(List<List<? extends TypeWrapper>> candidates, TypeWrapper[] match) {
        for (int j = 0; j < MAX_DEPTH; j++) {
            for (int index = 0; index < candidates.size(); index++) {
                List<? extends TypeWrapper> methodParams = candidates.get(index);
                if (methodParams.size() == match.length) {
                    if (this.isTypesTransformable(match, methodParams, j, match.length)) {
                        return index;
                    }
                }
            }
        }
        return -1;
    }

    /**
     * Verifica se os tipos podem ser transformados nos tipo passados, dada uma profundidade maxima
     * 
     * @param fromTypes os tipos de origem
     * @param methodParams parametros dos metodos
     * @param maxDepth int
     * @param n int
     * @return boolean
     */
    public boolean isTypesTransformable(TypeWrapper[] fromTypes, List<? extends TypeWrapper> methodParams, int maxDepth, int n) {
        for (int i = 0; i < n; i++) {
            TypeWrapper fromType = fromTypes[i];
            TypeWrapper toType = methodParams.get(i);
            if (!this.isTransformable(fromType, toType, maxDepth)) {
                return false;
            }
        }
        return true;
    }

    /**
     * @param from {@link TypeWrapper}
     * @param to {@link TypeWrapper}
     * @param maxDepth int
     * @return boolean
     */
    public boolean isTransformable(TypeWrapper from, TypeWrapper to, int maxDepth) {
        return this.findTransformationMatching(from, to, maxDepth) != null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#findTransformationMatching(bsh.meta.TypeWrapper, bsh.meta.TypeWrapper, int)
     */
    public List<TypeTransformation> findTransformationMatching(TypeWrapper fromType, TypeWrapper toType, int maxDepth) {
        for (int i = 0; i < maxDepth + 1; i++) {
            LinkedList<TypeTransformation> list = new LinkedList<TypeTransformation>();
            if (this.findTransformationRecurse(fromType, toType, i, list)) {
                return list;
            }
        }
        return null;
    }

    /**
     * Devolve uma lista com os {@link TypeTransformation} necessarios para converter de From para To com uma profundidade maxima de
     * maxDepth
     * 
     * @param fromType from
     * @param toType to
     * @param maxDepth max
     * @return {@link List}
     */
    public List<TypeTransformation> findTransformationMatching(Class<?> fromType, Class<?> toType, int maxDepth) {
        return this.findTransformationMatching(JavaClassWrapper.wrapperFor(fromType), JavaClassWrapper.wrapperFor(toType), maxDepth);
    }

    /**
     * Procura as transformacoes aplicaveis ateh conseguir converter
     * 
     * @param fromType from
     * @param toType to
     * @param maxDepth max
     * @param list List
     * @return boolean
     */
    private boolean findTransformationRecurse(TypeWrapper fromType, TypeWrapper toType, int maxDepth, LinkedList<TypeTransformation> list) {
        if (toType.isSuperTypeOf(fromType)) {
            return true;
        }
        if (maxDepth == 0) {
            return false;
        }
        for (TypeTransformation typeTransformation : this.allTransformations) {
            if (typeTransformation.acceptsType(fromType)) {
                list.add(typeTransformation);
                TypeWrapper result = typeTransformation.toType(fromType);
                if (result.equals(toType)) {
                    return true;
                } else {
                    // try another depth
                    if (this.findTransformationRecurse(result, toType, maxDepth - 1, list)) {
                        return true;
                    }
                }
                list.removeLast();
            }
        }
        for (TypeTransformationFactory factory : this.transformationFactories) {
            if (factory.isApplicable(fromType, toType)) {
                TypeTransformation another = factory.getTypeTransformation(fromType, toType);
                list.add(another);
                return true;
            }
        }
        return false;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#findRequiredField(bsh.meta.TypeWrapper, java.lang.String)
     */
    public FieldDescriptor findRequiredField(TypeWrapper targetType, String field) {
        for (FieldDescriptor fieldDescriptor : targetType.visibleFields()) {
            if (fieldDescriptor.fieldName().equals(field)) {
                return fieldDescriptor;
            }
        }
        throw new CompilationAssertionFailed("no field named: " + field + " on type: " + targetType);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isIntPrimitive(TypeWrapper top) {
        Class<?> type = JavaClassWrapper.unwrapJavaClass(top);
        return type == int.class || type == short.class || type == char.class || type == byte.class;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#isBooleanPrimitive(bsh.meta.TypeWrapper)
     */
    public boolean isBooleanPrimitive(TypeWrapper typeWrapper) {
        return JavaClassWrapper.unwrapJavaClass(typeWrapper) == boolean.class;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#findTransformationMatching(bsh.meta.TypeWrapper, bsh.meta.TypeWrapper)
     */
    public List<TypeTransformation> findTransformationMatching(TypeWrapper fromType, TypeWrapper toType) {
        return this.findTransformationMatching(fromType, toType, MAX_DEPTH);
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#findApplicableTransformations(bsh.meta.TypeWrapper)
     */
    public Collection<TypeTransformation> findApplicableTransformations(TypeWrapper targetType) {
        List<TypeTransformation> list = new LinkedList<TypeTransformation>();
        for (TypeTransformation typeTransformation : this.allTransformations) {
            if (typeTransformation.acceptsType(targetType)) {
                list.add(typeTransformation);
            }
        }
        return list;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#findConstructorFor(bsh.meta.TypeWrapper, bsh.meta.TypeWrapper[])
     */
    public List<? extends TypeWrapper> findConstructorFor(TypeWrapper declaring, TypeWrapper[] argTypes) {
        List<List<? extends TypeWrapper>> visibleConstructors = new ArrayList<List<? extends TypeWrapper>>(declaring.visibleConstructors());
        int i = this.findBestMatch(visibleConstructors, argTypes);
        if (i >= 0 && i < visibleConstructors.size()) {
            return visibleConstructors.get(i);
        }
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#findTransformationMatchingIncluding(bsh.meta.TypeWrapper, bsh.meta.TypeWrapper,
     * bsh.utils.reflection.transform.TypeTransformationFactory[])
     */
    public List<TypeTransformation> findTransformationMatchingIncluding(TypeWrapper fromType, TypeWrapper toType,
            TypeTransformationFactory... factories) {
        if (factories.length == 0) {
            return this.findTransformationMatching(fromType, toType);
        }
        ReflectionUtilsImpl copy = new ReflectionUtilsImpl();
        for (TypeTransformationFactory typeTransformationFactory : factories) {
            copy.transformationFactories.add(typeTransformationFactory);
        }
        return copy.findTransformationMatching(fromType, toType);
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#getMethodWithName(java.lang.Class, java.lang.String)
     */
    public MethodDescriptor getMethodWithName(Class<?> class1, String string) {
        Method[] methods = class1.getMethods();
        for (Method method : methods) {
            if (method.getName().equals(string)) {
                return new JavaMethodDescriptor(method);
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#isEnumInstance(java.lang.Object)
     */
    public boolean isEnumInstance(Object obj) {
        return Enum.class.isInstance(obj);
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.ReflectionUtils#getEnumType(java.lang.Object)
     */
    public Class<?> getEnumType(Object instance) {
        if (!this.isEnumInstance(instance)) {
            return null;
        }
        Class<?> theClass = instance.getClass();
        while (!theClass.isEnum()) {
            theClass = theClass.getSuperclass();
        }
        return theClass;
    }

    /**
     * {@inheritDoc}
     */
    public FieldDescriptor findField(TypeWrapper targetType, String field) {
        for (FieldDescriptor fieldDescriptor : targetType.visibleFields()) {
            if (fieldDescriptor.fieldName().equals(field)) {
                return fieldDescriptor;
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public MethodDescriptor findExactMatch(TypeWrapper type, String methodName, TypeWrapper[] typeWrappers) {
        for (MethodDescriptor md : type.visibleMethods()) {
            if (md.methodName().equals(methodName) && md.parameterTypes().size() == typeWrappers.length
                    && Arrays.equals(md.parameterTypes().toArray(new TypeWrapper[typeWrappers.length]), typeWrappers)) {
                return md;
            }
        }
        return null;
    }

}
